※ 以下内容は未検証&改善点が多く存在します。随時アップデート
実装のみ確認する場合は飛ばして
- LFSクライアント&サーバーの仕組み
に移動
LFSサーバーサンプルソースは
はじめに
Gitで100MBを超えるファイルを扱いたい場合、一つの手段としてGit LFSを扱うこともできます。 GIt LFSについては以下の記事を参照
GitHub
GitHubでは 1GBの容量&帯域までは無料としていますがそれを超えた場合
- 50GBの帯域+ストレージ は 5ドル/月
が発生します。 150GB借りる場合は月に15ドル
帯域にも料金が発生するため、例えば10GBのリポジトリを月に5人落とすだけで限界。
GitHub LFS料金について
Git LFSにはデータを保存するストレージ先を任意に変更できるため、Google Cloud Storageを利用します。 Google Cloud Storageの料金体系は以下 https://cloud.google.com/storage/pricing?hl=ja
- リージョン選択は アイオナ(us-central1)
- Standard Storage
- 10GBを利用想定
- 月に10人がClone想定
[0.020ドル(保管料)] + [0.12ドル(下り) x 10(GB) x 10(人)] = 12.2ドル 大体1500円ほどかかる想定です。(オペレーションは無視) しかし、10人クローンするのが初月だけであれば あとは保管料の 0.020ドル+α しか発生しないため GitHub 利用料金と比べたらはるかに安く済みます
※上記は皮算用のため正確な料金は計算ツールで確認してください https://cloud.google.com/products/calculator?hl=ja
また実装について以下サイト様は非常に参考になりました。LFS&LFSサーバーの仕組みを理解するのに一読をおすすめします git-lfsの仕様(サーバー側)を個人的に解説してみる https://jyn.jp/git-lfs-api/ APIGateway+Lambda+S3で格安GitLFSサーバーを運用する【使い方の紹介と車輪の再発明_:(´ཀ`」 ∠):】 https://sakataharumi.hatenablog.jp/entry/2022/09/29/223025 Git LFSをAmazon S3でいい感じにする話 https://ydkk.hateblo.jp/entry/2017/12/07/120000
※ ちなみに、AmazonS3であれば記事も豊富で上記サイトにテンプレートもあるためS3の方が楽です
AmazonS3, GoogleCloudStorage をPythonで両対応したオープンソースの giftless を利用するのもありだと思います Giftless https://giftless.datopian.com/en/latest/index.html
LFSクライアント&サーバーの仕組み
- API構築に Google API Gateway
- LFSサーバーに Google Function
- LFSストレージ先に Google Cloud Storage
- 言語は **.Net 6.0 **
を利用します
参考
詳細はLFSリポジトリを確認してください https://github.com/git-lfs/git-lfs Git LFS Batch API https://github.com/git-lfs/git-lfs/blob/main/docs/api/batch.md Basic Transfers https://github.com/git-lfs/git-lfs/blob/main/docs/api/basic-transfers.md
LFSクライアントの準備
公式Readmeの Installing を参考にGit LFSをインストール https://github.com/git-lfs/git-lfs#installing
リポジトリの準備
テスト用のファイルを用意 約104MBある画像を作成しました。作成した画像をLFSストレージに置くことを目指します。 ※以下参考 https://www.wakuwakubank.com/posts/400-mac-dummy-image/
BatchAPIについて
100MBのリソースをPushするとき、gitはアップロード先を教えてもらう為にLFSサーバーに問い合わせを行います。 その時以下のようなJsonが一緒に来る (必要なものだけ抜き出し)
// POST https://lfs-server.com/objects/batch
// Accept: application/vnd.git-lfs+json
// Content-Type: application/vnd.git-lfs+json
// Authorization: Basic ... (if needed)
{
"operation": "upload", // ダウンロード or アップロードどちらの情報がほしいか
"objects": [ // ファイルの詳細
{
"oid": "12345678", // LFSファイルの一意なID
"size": 123 // LFSファイルのサイズ
}
]
}
これをLFSサーバーが受けてレスポンスとして以下のようなJsonをクライアントに返せばOKです
// HTTP/1.1 200 Ok
// Content-Type: application/vnd.git-lfs+json
{
"transfer": "basic", // なくてもいい
"objects": [
{
"oid": "1111111", // リクエストで来たoidをそのまま返せばいい
"size": 123, // リクエストできたサイズをそのまま返せばいい
"authenticated": true, // 認証済みということにする
"actions": {
// ダウンロードについての情報であれば "download"
// アップロードについての情報であれば "upload" とする
"download": {
"href": "https://some-download.com", // 格納先URL
// クライアントからストレージへの通信のheaderに何か付与したい場合書く。
// なくてもいい
"header": {
"Key": "value"
},
// 以下はどちらかでOK。アクセス可能期間
"expires_in" : 86400,
"expires_at": "2016-11-10T15:29:07Z"
}
}
}
],
// なくていい。デフォルトsha256
"hash_algo": "sha256"
}
- Git LFS クライアント → LFSサーバー
にストレージ先を問い合わせ。そのURLを利用して
- Git LFS クライアント → LFSストレージ
にアップロード / ダウンロードする流れ
アップロード/ダウンロードするときの処理は Basic Transfer API が担当 https://github.com/git-lfs/git-lfs/blob/main/docs/api/basic-transfers.md ※流す処理をカスタマイズしなくていい場合は特にBasicから変えなくて良いです
LFSサーバー
Google Cloud コンソールから必要なものを作成していきます
プロジェクトの作成
これがないと始まらないため作成 ■プロジェクトの作成 https://cloud.google.com/resource-manager/docs/creating-managing-projects#api また、CLIもインストール推奨 https://cloud.google.com/sdk/docs/install-sdk
サービスアカウント作成もしておきます。 特定プロジェクトの特定権限を与えたアカウントであり、この作成したアカウントを利用してGCP内の操作を行います。 https://cloud.google.com/iam/docs/service-accounts?hl=ja
Http Cloud Functionsの作成
サーバープログラムの起動場所としてCloudFunctionsを作成 公式ページを参考に構築 ■.Netでhttp Cloud Function https://cloud.google.com/functions/docs/create-deploy-http-dotnet ■Google Cloud CLI を使用して Cloud Functions(第 1 世代)の関数を作成してデプロイする https://cloud.google.com/functions/docs/create-deploy-gcloud-1st-gen?hl=ja
※最終的にデプロイするコードは以下のGitHubに載せてあります https://github.com/Toshiki-Sakamoto/GitLFS_GoogleCloudStorage_Sample
※公式は** .Net Core 3.1 **で構築していっていますが現在であれば .Net6以上が良いので、projを開いて TargetFramework を 6.0 に変更しています
依存関係には Funcions.Hosting に加え、CloudStorageも追加。
コードを変更するたびにデプロイが必要になります。
関数のデプロイ方法には
- GoogleCloudコンソールからzipでアップロードする方法
- gloudコマンドを利用してローカルからアップロードする方法
が取れますが、 Zipにする手間に加え、Zipでアップロードした場合 512kb を超えているとCloud Functinos からソースが見れなるためコマンドの利用をお勧めします。
(コマンド例)
gcloud functions deploy [任意Funcion名] --entry-point [エントリポイント] --runtime dotnet6 --trigger-http --allow-unauthenticated --source .
.Netのエントリポイントは [Namespace名].[クラス名]
※ コマンドでデプロイする場合 —source 引数を指定していなければ反映されない不具合(?)があります。 一見デプロイされているように見えますが実行される内容は変わっていない。ということが起きたので注意です https://stackoverflow.com/questions/47873446/how-do-i-update-google-cloud-function-source
デプロイすると Cloud Functions の一覧に表示
その他
複数のプロジェクトが存在するときの管理、切り替えについて https://cloud.google.com/sdk/docs/configurations
API Gateway
APIを定義して特定のURLでアクセスした時に Function を実行してもらいます
■API Gateway と Cloud Functions のスタートガイド https://cloud.google.com/api-gateway/docs/get-started-cloud-functions?hl=ja
API定義のyamlファイルに最低限必要なものは以下のようになります
# openapi2-functions.yaml
swagger: '2.0'
info:
title: gcs-lfs-server GCS
description: Sample API on API Gateway with a Google Cloud Functions backend
version: 1.0.0
schemes:
- https
produces:
- application/json
paths:
/[任意名]/objects/batch: # ※1
post:
summary: git lfs upload and download proxy
operationId: [任意名]
# 実行するFunctionを定義
x-google-backend:
address: https://[リージョン]-[プロジェクトID].cloudfunctions.net/[実行するFunction名]
responses:
'200':
description: A successful response
schema:
type: string
※1 の部分がURL。 LFSは **[LFSサーバーURL] の末尾に /objects/batch **をつけて呼び出すのが注意です。 LFSサーバーの任意名のみだとアクセスされません (本当はワイルドカードを利用して [任意名]+α のURLを許可するのが良さそう)
これで外部から [LFSサーバーURL]/objects/batch にアクセスした時に記述したFunctionが呼び出されます
Google Cloud Storage設定
LFSリソース保存先の設定を行います
バケットを作成します 作成時の設定により料金も少し変わるため設定見つつ。 今回は単体リージョンでStandardを利用しています
.Netコード構築
BatchAPIがアクセスしてきた場合、
- ダウンロード先URL
- アップロード先URL
を返すだけのコードを構築します
■Cloud Storage のツールを使用した V4 署名プロセス https://cloud.google.com/storage/docs/access-control/signing-urls-with-helpers?hl=ja#storage-signed-url-object-csharp
大した量ではないので全部載せます ※ 現状エラー処理など怠ってます
using Google.Cloud.Functions.Framework;
using Microsoft.AspNetCore.Http;
using Google.Cloud.Storage.V1;
using System.Threading.Tasks;
using System;
using System.Net.Http;
using System.IO;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http.Extensions;
using System.Xml.Linq;
using System.Linq;
namespace GDriveLFS
{
/// <summary>
/// 署名付きURL
/// </summary>
public class V4SignedUrlGenerator
{
public string GenerateV4SignedReadUrl(
string bucketName,
string objectName,
string credentialFilePath)
{
UrlSigner urlSigner = UrlSigner.FromServiceAccountPath(credentialFilePath);
string url = urlSigner.Sign(bucketName, objectName, TimeSpan.FromHours(1), HttpMethod.Get);
return url;
}
public string GenerateV4UploadSignedUrl(
string bucketName,
string objectName,
string credentialFilePath)
{
UrlSigner urlSigner = UrlSigner.FromServiceAccountPath(credentialFilePath);
var options = UrlSigner.Options.FromDuration(TimeSpan.FromHours(1));
var template = UrlSigner.RequestTemplate
.FromBucket(bucketName)
.WithObjectName(objectName)
.WithHttpMethod(HttpMethod.Put);
string url = urlSigner.Sign(template, options);
return url;
}
}
public class GCSStorageController
{
public const string BacketName = "[gcsのバケット名]";
public const string CredentialFileName = "[アカウントキー.jsonファイル名]"; // credential file (with private keys)
private V4SignedUrlGenerator _urlGenerator = new V4SignedUrlGenerator();
private string _credentialFilePath;
public string Path => $"{Directory.GetCurrentDirectory()}/{CredentialFileName}";
public void Setup()
{
while (!File.Exists(Path))
{
var path = Directory.GetParent(Directory.GetCurrentDirectory()).FullName;
Directory.SetCurrentDirectory(path);
}
_credentialFilePath = Path;
}
public string GetDownloadURL(string objectName)
{
if (string.IsNullOrEmpty(_credentialFilePath)) return string.Empty;
var result = _urlGenerator.GenerateV4SignedReadUrl(BacketName, objectName, _credentialFilePath);
return result;
}
public string GetUploadURL(string objectName)
{
if (string.IsNullOrEmpty(_credentialFilePath)) return string.Empty;
var result = _urlGenerator.GenerateV4UploadSignedUrl(BacketName, objectName, _credentialFilePath);
return result;
}
}
public class RequestBody
{
public class Object
{
public string oid { get; set; }
public int size { get; set; }
}
public string operation { get; set; }
public string[] transfers { get; set; }
public Object[] objects { get; set; }
public string hash_algo { get; set; }
}
public class ResponseBody
{
public class URL
{
public string href { get; set; }
public int expires_in { get; set; }
public static URL CreateAtDownload(string oid) =>
new URL { href = Function.StorageController.GetDownloadURL(oid), expires_in = 86400 };
public static URL CreateAtUpload(string oid) =>
new URL { href = Function.StorageController.GetUploadURL(oid), expires_in = 86400 };
public static URL CreateAtEmpty() =>
new URL { href = "" };
}
public class Action
{
public URL upload { get; set; }
public URL download { get; set; }
public static Action CreateAtUpload(string oid) =>
new Action() { upload = URL.CreateAtUpload(oid), download = URL.CreateAtEmpty() };
public static Action CreateAtDownload(string oid) =>
new Action() { download = URL.CreateAtDownload(oid), upload = URL.CreateAtEmpty() };
}
public class Object
{
public string oid { get; set; }
public int size { get; set; }
public bool authenticated { get; set; } = true;
public Action actions { get; set; }
public static Object CreateAtUpload(string oid, int size) =>
new Object { oid = oid, size = size, actions = Action.CreateAtUpload(oid) };
public static Object CreateAtDownload(string oid, int size) =>
new Object { oid = oid, size = size, actions = Action.CreateAtDownload(oid) };
}
public string transfer { get; set; } = "basic";
public Object[] objects { get; set; }
public static ResponseBody CreateAtUpload(RequestBody.Object[] objects) =>
new ResponseBody { objects = objects.Select(x => Object.CreateAtUpload(x.oid, x.size)).ToArray() };
public static ResponseBody CreateAtDownload(RequestBody.Object[] objects) =>
new ResponseBody { objects = objects.Select(x => Object.CreateAtDownload(x.oid, x.size)).ToArray() };
}
public class Function : IHttpFunction
{
public static GCSStorageController StorageController;
private readonly ILogger _logger;
public Function(ILogger<Function> logger) =>
_logger = logger;
public async Task HandleAsync(HttpContext context)
{
var request = context.Request;
// If there's a body, parse it as JSON and check for "message" field.
using TextReader reader = new StreamReader(request.Body);
string text = await reader.ReadToEndAsync();
if (text.Length <= 0)
{
_logger.LogError("RequestBody Not found");
return;
}
_logger.LogInformation($"*** Request: {text}");
try
{
var requestBody = JsonSerializer.Deserialize<RequestBody>(text);
StorageController = new GCSStorageController();
StorageController.Setup();
var response = default(ResponseBody);
switch (requestBody.operation)
{
case "upload":
response = ResponseBody.CreateAtUpload(requestBody.objects);
break;
case "download":
response = ResponseBody.CreateAtDownload(requestBody.objects);
break;
default: // verify
return;
}
var responseBody = JsonSerializer.Serialize<ResponseBody>(response);
_logger.LogInformation($"*** Response: {responseBody}");
context.Response.ContentType = "application/vnd.git-lfs+json";
await context.Response.WriteAsJsonAsync<ResponseBody>(response);
}
catch (JsonException parseException)
{
_logger.LogError(parseException, "Error parsing JSON request");
}
}
}
}
特筆すべきところは UrlSigner
を利用してURLを生成しているところです。
ここに指定する credentialFile
の中にある認証情報を利用して発行。
権限を持ったサービスアカウントを利用して作成します。 以下参照 ■サービス アカウント キーの作成と管理 https://cloud.google.com/iam/docs/creating-managing-service-account-keys#creating_service_account_keys ■認証のスタートガイド https://cloud.google.com/docs/authentication/getting-started?hl=ja
※サービスアカウントキー.jsonをローカルにおいて認証するのは推奨されてません 環境変数として用意するのが良さげ (とりまということで楽だったので.. 改修します)
もしローカルにおいたjsonを参照する場合は、 VisualStudioから対象.jsonを右クリックして **クイックプロパティ > 出力ディレクトリにコピー ** をして実行時に CurrentDirectory でとれるようにしておきます。 projファイルに設定が追加されるはずです。 (上記のような操作した後もデプロイを忘れないこと)
リポジトリのLFS設定
サーバー側の設定が終わったので、gitでLFS操作をした時クライアントからLFSサーバーに繋いでもらう必要があります
lfs公式サイトを見て設定 ■lfs https://github.com/git-lfs/git-lfs#example-usage
- LFS化するファイルの指定
.gitattribute設定がされること。 以下は jpg, png をLFS対象にする
*.jpg filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
- LFSサーバーの指定
以下公式にある通り .config ファイルにURLが記載されているとそこの繋ぎにいきます ■Server Discovery https://github.com/git-lfs/git-lfs/blob/main/docs/api/server-discovery.md
コマンドを利用してLFSサーバーのURLを設定
git config --file=.lfsconfig lfs.url [URL(objects/batchは含めない)]
できたファイルはプロジェクトで共有するためにコミットしておきましょう
これで接続は問題ないため実際にPush エラーが出たら Cloud Function の ログを見て確認